typedef struct { long firstThing; long totalThings; }sWorkParams, *sWorkParamsPtr; typedef struct { MPTaskID taskID; MPQueueID requestQueue; MPQueueID resultQueue; sWorkParams params; }sTaskData, *sTaskDataPtr; long gNumProcessors; sTaskDataPtr gTaskData; MPQueueID gNotificationQueue; void fStartMP( void ) { OSErr theErr; long i; theErr = noErr; /* Assume single processor mode */ gNumProcessors = 1; /* Initialize remaining globals */ gTaskData = NULL; gNotificationQueue = NULL; /* If the library is present create the tasks (no tasks on a */ /* single CPU system) */ if( MPLibraryIsLoaded() ) { gNumProcessors = MPProcessors(); gTaskData = (sTaskDataPtr)NewPtrClear( (gNumProcessors - 1) * sizeof( sTaskData ) ); theErr = MemError(); if( theErr == noErr ) theErr = MPCreateQueue( &gNotificationQueue ); for( i = 0; i < gNumProcessors - 1 && theErr == noErr; i++ ) { if( theErr == noErr ) theErr = MPCreateQueue( &gTaskData[i].requestQueue ); if( theErr == noErr ) theErr = MPCreateQueue( &gTaskData[i].resultQueue ); if( theErr == noErr ) theErr = MPCreateTask( fTask, &gTaskData[i], kMPUseDefaultStackSize, gNotificationQueue, NULL, NULL, kMPNormalTaskOptions, &gTaskData[i].taskID ); } } /* If something went wrong, just go back to single processor */ /* moden */ if( theErr != noErr ) { fStopMP(); gNumProcessors = 1; } }
The structure sWorkParams defines the parameters that will be passed into the function called by the task. The content of this block is specific to the type of work being performed. Among other things, the parameters should define the specific data that the function is to process. The structure sTaskData defines the block of data that the application will use to communicate a variety of different information to a task. The two main things being communicated are the queue IDs and work function parameters. The global gNumProcessors stores a count of the number of processors found in the system. This variable is set to one if the Multiprocessing API Library is not loaded or if task or queue creation fails for any reason. The rest of the application code is fashioned in such a way that if gNumProcessors is one then the application will do all the work itself and never make any Multiprocessing API calls. The global gTaskData points to a dynamically allocated array of sTaskData blocks. There is one entry for each task to be created. The global gNotificationQueue is used to receive notification messages from terminating tasks. All the tasks share one notification queue in this example. A number of tasks equal to the number of processors minus one are created. Each task has its own pair of message queues by which the application can communicate with it. The IDs of the queues are stored in the gTaskData entry for the task. The task is then created using MPCreateTask. The first parameter is a pointer to the function that will become the running task. In this example all the tasks share the same function:fTask. If a function can be correctly executed by multiple processors at once it is called 'reentrant'. Note that 'interrupt-safe' does not necessarily imply 'reentrant'. Interrupts generally are not interruptable in the Mac environment and engineers sometimes take advantage of this. However, in an MP environment you must anticipate that there could be tasks simultaneously executing at any point at any time within your task code. The second parameter is a pointer to the task's gTaskData entry. The task will be able to extract the IDs of the request and result queues it should use from this block. Note that two queues per task is often unnecessary. In many cases it is possible to use two queues total. All requests are posted to one queue and all results are returned on another queue. This works when it is irrelevant which task processes which request, as is often the case. Note, however, that the parameters for each task must be either completely contained within the message, or preestablished for every task prior to submitting the first request. The third parameter is the desired stack size for the task. Each task has its own stack. If you are going to be creating more than a handful of tasks, you should consider limiting the size of the stack each one will receive. The default size is 64K, which can seriously impact the amount of memory available to the Multiprocessing API Library if large numbers of tasks are going to be created. If you do specify the stack size, be sure to allocate at least as much space as your task's deepest call chain will require. The fourth parameter is for an optional notification queue. This queue is very important during task termination sequences. In fact, it really isn't optional unless you tightly coordinate task termination with the task itself. If you terminate a task without warning, you will definitely need a notification queue. The reason for this will be given later. The fifth and sixth parameters are returned on the notification queue when the task is terminated. The seventh parameter is for modifying the nature of task creation. There are no options available at this time. The eighth parameter is filled in by If anything goes wrong during task creation, The following is an example task. The first thing it does is
establish a pointer to its gTaskData entry which was specified in
|
#define kMyRequestOne 1 #define kMyRequestTwo 2 #define kMyResultException -1 OSStatus fTask( void *parameter ) { OSErr theErr; sTaskDataPtr p; Boolean finished; long message; theErr = noErr; /* Get a pointer to this task's unique data */ p = (sTaskDataPtr)parameter ; /* Process each request handed to the task and return a result */ finished = false; while( !finished ) { theErr = MPWaitOnQueue( p->requestQueue, (void **)&message, NULL, NULL, kDurationForever ); if( theErr == noErr ) { /* Pick a function to call and pass in the parameters. */ /* The parameters should be set up prior to sending the */ /* message just received. Note that we could also just */ /* pass in a pointer to the desired function instead of */ /* using a selector. */ switch( message ) { case kMyRequestOne: theErr = fMyTaskFunctionOne( &p->params ); break; case kMyRequestTwo: theErr = fMyTaskFunctionTwo( &p->params ); break; default: finished = true; theErr = kMyResultException; } MPNotifyQueue( p->resultQueue, (void *)theErr, NULL, NULL ); } else finished = true; } /* Task is finished now */ return( theErr ); }
Using Tasks To Perform WorkWhen your application needs to perform some work, it should make sure everything the tasks are going to need is in memory. For each task, the application will establish the parameters of the work that it wants the task to perform and then it will signal the task through either a queue or a semaphore to begin performing that work. The specific work that the task should perform can be completely defined within a message, or possibly in a block of memory reserved for that task as described above. Both methods are in common use. Some applications also pass in a pointer to the function that the task should call to perform the work. That way one task can perform many different types of chores. Once the task has been signaled, the application can help out with the work, or it could return to its event loop and just check in on the tasks from time to time using kDurationImmediate waits. When kDurationImmediate is specified to either
Therefore, if the application is checking for task results in its event loop, use kDurationImmediate waits and check the return value. If it is noErr, a result was present and obtained by the call. If it is kMPTimeoutErr, then the tasks have generated no new results since the last time the application checked. Don't forget that other kinds of errors could be returned also. As described above, when a task finishes handling the request, it should post a result to let the application know that the work has been performed. An example of an application using tasks to perform work follows.
In this case, the application is going to perform part of the work
also. Note that events are not being handled, so it is assumed that
|
OSErr fDoMP( long realFirstThing, long realTotalThings ) { long i; OSErr theErr; long thingsPerTask; long message; sWorkParams appData; theErr = noErr; thingsPerTask = realTotalThings / gNumProcessors; /* Start each task working on a unique piece of the total data */ for( i = 0; i < gNumProcessors - 1; i++ ) { gTaskData[i].params.firstThing = realFirstThing + thingsPerTask * i; gTaskData[i].params.totalThings = thingsPerTask; message = kMyRequestOne; MPNotifyQueue( gTaskData[i].requestQueue, (void *)message, NULL, NULL ); } /* Let the application do whatever is left over. Note that if */ /* gNumProcessors is one, then the application will do everything */ /* and the Multiprocessing API Library will not be called. */ appData.firstThing = realFirstThing + thingsPerTask * i; appData.totalThings = realTotalThings - thingsPerTask * i; fMyTaskFunctionOne( &appData ); /* Now wait for the tasks to finish */ for( i = 0; i < gNumProcessors - 1; i++ ) MPWaitOnQueue( gTaskData[i].resultQueue, (void **)&message, NULL, NULL, kDurationForever ); return( theErr ); }
This particular approach is used in a lot of real world applications. It is best suited to applications that transform large blocks of data. Data is split into even pieces for the tasks, they are started, and the remaining potentially uneven piece is processed by the application. Once the application has processed its piece, it then waits for the tasks to finish. A common mistake, even for experienced engineers, is to assume that the data will be perfectly divisible by the number of processors. Applications that work on large uneven pieces, such as a development environment trying to compile multiple files simultaneously, need to approach the problem differently. The application should sit in its event loop and as each task finishes the work it was assigned, new work, if any, should be assigned to the task. Terminating TasksWhen your application finishes it should call
The function |
void fStopMP( void ) { long i; if( gTaskData != NULL ) { for( i = 0; i < gNumProcessors - 1; i++ ) { if( gTaskData[i].taskID != NULL ) { MPTerminateTask( gTaskData[i].taskID, noErr ); MPWaitOnQueue( gNotificationQueue, NULL, NULL, NULL, kDurationForever ); } if( gTaskData[i].requestQueue != NULL ) MPDeleteQueue( gTaskData[i].requestQueue ); if( gTaskData[i].resultQueue != NULL ) MPDeleteQueue( gTaskData[i].resultQueue ); } if( gNotificationQueue != NULL ) { MPDeleteQueue( gNotificationQueue ); gNotificationQueue = NULL; } DisposePtr( (Ptr)gTaskData ); gTaskData = NULL; } }
Multiprocessing Do's and Don'tsDoIf you get a message at startup telling you that the MPLibrary
could not load because it was out of memory, then you should open
your copy of Metronub, which resides in the extension folder, using
ResEdit or Resourcer. Change the Tasks should call functions that perform faceless processing. Calculation intensive code is really the only type of code that should be considered for MP tasks. This rule will be substantially relaxed under Mac OS 8, but even so, throughput will really only be improved by using MP for calculation intensive code. Under Mac OS 8, responsiveness will be the main thing improved by multitasking other types of code. The work performed by a task between request and result signals should be 'substantial'. It can take several hundred machine cycles to send or receive a signal via one of the synchronization methods. If your task only takes a few cycles to complete the work that is requested, your application's performance is going to be dramatically worse with multiprocessing. Tasks should try to consume at least a million cycles per request. That's 5 milliseconds on a 200MHz processor and 20 times faster than necessary to maintain the tenth of a second response time quoted throughout this document. If your task needs to allocate memory, you will have to either
allocate the memory prior to signaling the task, or use the function
Don'tDon't attempt to call 68K code. There is no emulator on the secondary processors and they will fault if they attempt to perform a mixed mode switch. Do not call the Toolbox. The Toolbox still contains
large amounts of 68K code, but even worse it is largely
non-reentrant. For example if one task is calling Do not call into unknown code. If you provide a means by which a third party can specify a callback, then do not attempt to call that function from a task. There is no telling what the callback is going to do. This rule will never be relaxed. Unless you specifically require that the callback be reentrant, then there is always going to be the possibility that it is not. Avoid globals. The main cause of non-reentrancy is the manipulation of globals. Tasks that manipulate globals, global state, or buffers pointed to by globals must use synchronization techniques to prevent other tasks from attempting to do so at the same time. Globals that are read only are fine. Do not call any MP API routines at interrupt time. The Multiprocessing API Library is not, strictly speaking, reentrant. While you can call any Multiprocessing API routine from a multiprocessing task any time, you may not call them from a deferred task, a time manager task or any other system interrupt handler. Workarounds exist but they are inefficient and generally discouraged. Contact Apple DTS or DayStar for more information. SummaryAfter reading this Technote, you should be comfortable with the basic steps involved in producing a multiprocessing-aware application. In short, you need to make sure the Multiprocessing API Library is available, you need to count the number of processors, you need to create the means by which to synchronize with tasks, and you need to create a sufficient number of tasks that will keep all the processors busy. Unique information can be communicated to a task when it is created that will allow a task to coordinate with the application when work needs to be performed. When your application quits, it should delete the synchronization objects and terminate the tasks. You should be familiar with the types of things a task can do and you should know what a task cannot do. Further References
|